import type { RendererOptions } from 'vue';
import { BufferAttribute } from 'three';
import type { Object3D, Camera } from 'three';

import { useLogger } from '../compositions/useLogger';
import { deepArrayEqual, isHTMLTag, kebabToCamel } from '../utils';
import { createInstancedMesh } from '../utils/createInstancedMesh';

import type { TresObject, TresObject3D, TresScene } from '../types';
import { catalogue } from './catalogue';

function noop ( fn: string ): any {
    fn;
}

function isFunction ( t ) {
    return typeof t === "function";
}

let scene: TresScene | null = null;

const { logError, logWarning } = useLogger();

export const nodeOps: RendererOptions<TresObject, TresObject> = {

    // 注册组件
    createElement ( tag, isSVG, anchor, props ) {

        if ( !props ) props = {};

        if ( !props.args ) {
            props.args = [];
        }

        // 如果模板里面存在HTML标签，template标签，返回null
        if ( tag === 'template' ) return null;
        if ( isHTMLTag( tag ) ) return null;


        let instance;

        if ( tag === 'primitive' ) {
            if ( props?.object === undefined ) logError( 'Tres primitives need a prop \'object\'' );
            const object = props.object as TresObject;
            tag = object.type;
            instance = Object.assign( object, { type: tag, attach: props.attach, primitive: true } );
        }
        else {

            const _tag = tag.split( "TT" )[ 1 ];

            const target = catalogue.value[ _tag ];

            if ( !target ) {
                logError( `${ tag } 不在 THREE 的命名空间，不存在 THREE.${ tag }` );
            }

            instance = new target( ...props.args );
        }

        if ( instance.isCamera ) {
            if ( !props?.position ) {
                instance.position.set( 3, 3, 3 );
            }
            if ( !props?.lookAt ) {
                instance.lookAt( 0, 0, 0 );
            }
        }

        // InstancedMesh 
        if ( props && "createInstancedMesh" in props ) {
            const { positions, rotations, scales } = props;
            instance = createInstancedMesh( instance, positions, rotations, scales );
        }

        if ( props?.attach === undefined ) {
            if ( instance.isMaterial ) instance.attach = 'material';
            else if ( instance.isBufferGeometry ) instance.attach = 'geometry';
        }
        // determine whether the material was passed via prop to
        // prevent it's disposal when node is removed later in it's lifecycle

        if ( instance.isObject3D ) {
            if ( props?.material?.isMaterial ) ( instance as TresObject3D ).userData.materialViaProp = true;
            if ( props?.geometry?.isBufferGeometry ) ( instance as TresObject3D ).userData.geometryViaProp = true;
        }

        // Since THREE instances properties are not consistent, (Orbit Controls doesn't have a `type` property) 
        // we take the tag tag and we save it on the userData for later use in the re-instancing process.
        instance.userData = {
            ...instance.userData,
            name: tag,
        };

        return instance;
    },

    // 插入组件
    insert ( child, parent ) {

        if ( parent && parent.isScene ) scene = parent as unknown as TresScene;

        const parentObject = parent || scene;

        if ( child && child.isInstancedMesh ) {
            parent.add( child );
        }

        if ( child?.isObject3D ) {

            if ( child?.isCamera ) {
                if ( !scene?.userData.registerCamera )
                    throw 'could not find registerCamera on scene\'s userData';

                scene?.userData.registerCamera?.( child as unknown as Camera );
            }

            if (
                child?.onClick
                || child?.onpointermove
                || child?.onpointerenter
                || child?.onpointerleave
            ) {
                if ( !scene?.userData.registerAtPointerEventHandler )
                    throw 'could not find registerAtPointerEventHandler on scene\'s userData';

                scene?.userData.registerAtPointerEventHandler?.( child as Object3D );
            }
        }

        if ( child?.isObject3D && parentObject?.isObject3D ) {
            parentObject.add( child );
            child.dispatchEvent( { type: 'added' } );
        }
        else if ( child?.isFog ) {
            parentObject.fog = child;
        }
        else if ( typeof child?.attach === 'string' ) {
            child.__previousAttach = child[ parentObject?.attach as string ];
            if ( parentObject ) {
                parentObject[ child.attach ] = child;
            }
        }
    },

    // 移除组件,释放three.js内存
    remove ( node ) {
        if ( !node ) return;
        // remove is only called on the node being removed and not on child nodes.

        if ( node.isObject3D ) {
            const object3D = node as unknown as Object3D;

            const disposeMaterialsAndGeometries = ( object3D: Object3D ) => {
                const tresObject3D = object3D as TresObject3D;

                if ( !object3D.userData.materialViaProp ) tresObject3D.material?.dispose();
                if ( !object3D.userData.geometryViaProp )
                    tresObject3D.geometry?.dispose();
            };

            const deregisterAtPointerEventHandler = scene?.userData.deregisterAtPointerEventHandler;
            const deregisterBlockingObjectAtPointerEventHandler
                = scene?.userData.deregisterBlockingObjectAtPointerEventHandler;

            const deregisterAtPointerEventHandlerIfRequired = ( object: TresObject ) => {

                if ( !deregisterBlockingObjectAtPointerEventHandler )
                    throw 'could not find deregisterBlockingObjectAtPointerEventHandler on scene\'s userData';

                scene?.userData.deregisterBlockingObjectAtPointerEventHandler?.( object as Object3D );

                if ( !deregisterAtPointerEventHandler )
                    throw 'could not find deregisterAtPointerEventHandler on scene\'s userData';

                if (
                    object?.onClick
                    || object?.onPointerMove
                    || object?.onPointerEnter
                    || object?.onPointerLeave
                )
                    deregisterAtPointerEventHandler?.( object as Object3D );
            };

            const deregisterCameraIfRequired = ( object: Object3D ) => {
                const deregisterCamera = scene?.userData.deregisterCamera;

                if ( !deregisterCamera )
                    throw 'could not find deregisterCamera on scene\'s userData';

                if ( ( object as Camera ).isCamera )
                    deregisterCamera?.( object as Camera );
            };

            object3D.traverse( ( child: Object3D ) => {
                disposeMaterialsAndGeometries( child );
                deregisterCameraIfRequired( child );
                deregisterAtPointerEventHandlerIfRequired?.( child as TresObject );
            } );

            disposeMaterialsAndGeometries( object3D );
            deregisterCameraIfRequired( object3D );
            deregisterAtPointerEventHandlerIfRequired?.( object3D as TresObject );
        }

        node.removeFromParent?.();

        node.dispose?.();
    },

    // 绑定属性
    patchProp ( node, prop, prevValue, nextValue ) {

        if ( node ) {
            let root = node;
            let key = prop;
            if ( node.isObject3D && key === 'blocks-pointer-events' ) {

                if ( nextValue || nextValue === '' )
                    scene?.userData.registerBlockingObjectAtPointerEventHandler?.( node as Object3D );
                else
                    scene?.userData.deregisterBlockingObjectAtPointerEventHandler?.( node as Object3D );

                return;
            }

            let finalKey = kebabToCamel( key );

            let target = root?.[ finalKey ];

            if ( key === 'args' ) {
                const prevNode = node as TresObject3D;
                const prevArgs = prevValue ?? [];
                const args = nextValue ?? [];
                const instanceName = node.userData.name || node.type;

                if ( instanceName && prevArgs.length && !deepArrayEqual( prevArgs, args ) ) {
                    root = Object.assign( prevNode, new catalogue.value[ instanceName ]( ...nextValue ) );
                }
                return;
            }

            if ( root.type === 'BufferGeometry' ) {
                if ( key === 'args' ) return;
                root.setAttribute(
                    kebabToCamel( key ),
                    new BufferAttribute( ...( nextValue as ConstructorParameters<typeof BufferAttribute> ) ),
                );
                return;
            }

            // Traverse pierced props (e.g. foo-bar=value => foo.bar = value)
            if ( key.includes( '-' ) && target === undefined ) {
                const chain = key.split( '-' );
                target = chain.reduce( ( acc, key ) => acc[ kebabToCamel( key ) ], root );
                key = chain.pop() as string;
                finalKey = key.toLowerCase();
                if ( !target?.set ) root = chain.reduce( ( acc, key ) => acc[ kebabToCamel( key ) ], root );
            }
            let value = nextValue;
            if ( value === '' ) value = true;
            // Set prop, prefer atomic methods if applicable
            if ( isFunction( target ) ) {
                if ( Array.isArray( value ) ) node[ finalKey ]( ...value );
                else node[ finalKey ]( value );
                return;
            }

            if ( !target?.set && !isFunction( target ) ) root[ finalKey ] = value;
            else if ( target.constructor === value.constructor && target?.copy ) target?.copy( value );
            else if ( Array.isArray( value ) ) target.set( ...value );
            else if ( !target.isColor && target.setScalar ) target.setScalar( value );
            else target.set( value );
        }
    },

    parentNode ( node ) {
        return node?.parent || null;
    },
    createText: () => noop( 'createText' ),
    createComment: () => noop( 'createComment' ),

    setText: () => noop( 'setText' ),

    setElementText: () => noop( 'setElementText' ),
    nextSibling: () => noop( 'nextSibling' ),

    querySelector: () => noop( 'querySelector' ),

    setScopeId: () => noop( 'setScopeId' ),
    cloneNode: () => noop( 'cloneNode' ),

    insertStaticContent: () => noop( 'insertStaticContent' ),
};
